# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
##
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
__docformat__ = "restructuredtext en"

import math
import time
import logging

Logger = logging.getLogger('PySolFC.Game.MoveAnimate')

from .settings import CONF_OPT
from .viewlib.constants import CURSOR_WATCH
from .gamelib import state


class mPoint(object):
    """
    Represent a coord point on the canvas. The point can be given a movement
    `mod` which then can be applied over many frames.
    """
    _mod = [0, 0]

    def __init__(self, x, y):
        """
        __init__(x, y) -> None

        :Parameters:
          x : int
            left coordinate

          y : int
            top coordinate

        :IVariables:
          x : int
            left coordinate

          y : int
            top coordinate
        """
        self.x, self.y = x, y

    def __setattr__(self, item, value):
        if item in ('x', 'y'):
            super(mPoint, self).__setattr__(item, value)
        else:
            raise AttributeError

    def set_mod(self, x, y):
        """
        set_mod(x, y) -> None

        :Parameters:
          x : int
            left coordinate move mod to apply

          y : int
            top coordinate move mod to apply

        :IVariables:
          _mod : tuple(int, int)
            x, y coordinate move mod to apply
        """
        super(mPoint, self).__setattr__('_mod', (x, y,))

    def do_mod(self):
        """
        do_mod() -> None

        :IVariables:
          x : int
            left coordinate with its move mod applied

          y : int
            top coordinate with its move mod applied
        """
        self.x += self._mod[0]
        self.y += self._mod[1]

    def __repr__(self):
        return self.x, self.y

    def __str__(self):
        return 'Point: x=%d  y=%d' % (self.x, self.y)


def __animate_get_values(game, frames):
    """
    __animate_get_values(self, frames) -> float, int

    Compute animate values for number of frames and seconds per frame.

    :Parameters:
      frames : int
        Number of animation frames. Default is 8 which is used with
        animations options value to compute the actual num frames.
    """
    SPF = .15 / 8   # seconds per frame
    frames = 8 if frames < 0 else frames  # total frames for move
    if all([CONF_OPT['animations'] == 10, game.move_state == game.S_INIT,
            frames > 4]):
        # Used internally in game preview to speed up initial dealing
        frames /= 2
    elif CONF_OPT['animations'] in [3, 4, 5]:
        frames = frames * {3: 3, 4: 8, 5: 16}.get(CONF_OPT['animations'])
        SPF /= 2
    Logger.debug("frames: %s, sec / frame: %s", frames, SPF)
    return float(frames), float(SPF)


def animate_play(game, from_stack, to_stack, cards, x, y, tkraise=True,
                 frames=-1, shadow=-1):
    """
    animate_play(from_stack, to_stack, cards, x, y,[, tkraise=True, frames=-1,
      shadows=-1]) -> bool

    Animate card play to another pile. When animations option or frames is 0,
    no animation is performed.

    :Parameters:
      from_stack : Stack
        Pile cards being played.
      to_stack : Stack
        Pile cards being built.
      cards : tuple(Card...)
        Cards being played.
      x : int
        Left coordinate of to_stack card.
      y : int
        Top coordinate of to_stack card.

    :Keywords:
      tkraise : bool
        Apply tkraise() to cards before animate.
      frames : int
        Number of animation frames. Default is 8 frames.
      shadows : int
        Draw shadow under cards being played. Default is 'shadow' option.

    :returns: animation was performed?
    """
    if game.is_preview:
        return
    animation = CONF_OPT['animations']
    if animation == 0 or frames == 0:
        return False
    frames, SPF = __animate_get_values(game, frames)
    # start animation
    if tkraise:
        for card in cards:
            card.raise_item()
    c0 = cards[0]
    dx, dy = (x - c0.x) / frames, (y - c0.y) / frames
    i = 1
    starttime = time.time()
    shadows = ()
    if not game.is_preview or (CONF_OPT['shadow'] if shadow < 0 else shadow):
        # create shadows in the first frame
        offset = game.images.shadow_offset
        shadows = from_stack.createShadows(cards, *offset)
    while i < frames:
        for s in shadows:
            s.move(dx, dy)
        for card in cards:
            card.move(dx, dy)
        game.canvas.update_idletasks()
        i += 1
        endtime = starttime + i * SPF
        sleep = endtime - time.time()
        if sleep >= 0.005:       # ahead with animation
            Logger.debug('sleep')
            time.sleep(sleep)
        elif sleep <= -0.75 * SPF:  # behind with animation
            Logger.debug('skip')
            i += 2 if frames > 4 and sleep < -1.5 * SPF else 1
    # last frame: delete shadows, move card to final position
    for s in shadows:
        s.delete()
    for card in cards:
        card.move(x - c0.x, y - c0.y)
    game.canvas.update_idletasks()
    return True


def animate_flip_and_play(game, from_stack, to_stack=None, frames=-1):
    """
    animate_flip_and_play(from_stack, to_stack, cards, x, y,[, frames=-1])
      -> bool

    Animate card flip and play on its pile or another pile. When animations
    option or frames is 0, no animation is performed. Animation is also not
    performed if from_stack is empty.

    :Parameters:
      from_stack : Stack
        Pile cards being played.
      to_stack : Stack
        Pile cards being built.

    :Keywords:
      frames : int
        Number of animation frames. Default is 8 frames.

    :returns: animation was performed?
    """
    if game.is_preview:
        return
    if CONF_OPT['animations'] == 0 or frames == 0 or not from_stack.cards:
        return False

    canvas = game.canvas
    card = from_stack.TOPCARD
    id_ = card._item
    w, h = game.images.size
    frames, SPF = __animate_get_values(game, frames)
    #SPF *= 2
    start = from_stack.TOPPOSITION
    if to_stack is None:
        stop = start
    else:
        stop = to_stack.TOPPOSITION
    trip = stop[0] - start[0], stop[1] - start[1]

    if trip == (0, 0):  # flip card in place
        ascent = 0, h / 10.0 / frames
        min_size = w / 10
        shrink = (w - min_size) / (frames - 1), 0
    elif trip[1] == 0:  # flip and move card horizontal
        ascent = 0, h / 10.0 / frames
        min_size = w / 10
        shrink = (w - min_size) / (frames - 1), 0
    elif trip[0] == 0:  # flip and move card vertical
        ascent = 0, w / 10.0 / frames
        min_size = h / 10
        shrink = 0, (h - min_size) / (frames - 1)
    else:
        return False

    move = [(t / frames / 2) for t in trip]
    card.raise_item()

    def animate(img, pos, s=1):
        pos.set_mod((shrink[0]) * s / 2 + move[0] + (-ascent[0] * s),
                    (shrink[1]) * s / 2 + move[1] + (-ascent[1] * s))
        fset = (0, int(frames), s,) if s == 1 else (int(frames), 0, s,)
        for nframe in range(*fset):
            starttime = time.time()
            # resize img
            image = img.squeeze(nframe, delta)
            canvas.itemconfig(id_, image=image)
            # move img
            pos.do_mod()
            card.move_to(int(pos.x), int(pos.y))
            canvas.update_idletasks()
            #{ sleep remainder of SPF
            t = (SPF - (time.time() - starttime)) * 1000  # millisecs
            if t > 0:
                time.sleep(t / 1000)

    delta = (float(shrink[0]), float(shrink[1]),)
    position = mPoint(*start)
    animate(card._image[int(card.face_up)], position)
    animate(card._image[int(not card.face_up)], position, -1)
    card.move_to(*stop)
    return True


@state.not_state('is_preview')
def animate_redeal(cls):
    """
    animate_redeal() -> None

    Animate a redeal event at the start of game by bringing a random set
    of cards and the bottom 2 cards of each stack to the talon coords.
    """
    if not CONF_OPT['animations'] or not CONF_OPT['redeal_animation']:
        return
    cards = []
    for s in (p for p in cls.allstacks if p is not cls.s['talon']):
        cards.extend((c, s,) for c in s.cards)
    if not cards:
        return
    cls.app.top.wm_set_cursor(cursor=CURSOR_WATCH)
    cls.top.update()
    cls.canvas.update_idletasks()
    old_rate = CONF_OPT['animations']
    adj_rate = 2 if CONF_OPT['animations'] > 3 else 0
    CONF_OPT['animations'] = int(CONF_OPT['animations']) - adj_rate
    # animate
    sx, sy = cls.s['talon'].x, cls.s['talon'].y
    w, h = cls.width, cls.height
    from random import choice
    acards = []
    cards = cards[:]
    # Select random cards to animate
    for i in range(min(8, len(cards))):
        card = choice(cards)
        acards.append(card[0]._image[1])
        cards.remove(card)
    while cards:
        # get and un-tuple a random card
        t = choice(cards)
        c, s = t
        s.removeCard(c, update=False)
        # animation
        if c in acards or len(cards) <= 2:
            animate_play(cls, s, None, [c], w / 2, h / 2, tkraise=False,
                                shadow=0)
            animate_play(cls, s, None, [c], sx, sy, tkraise=False,
                                shadow=0)
        else:
            c.move_to(sx, sy)
        cards.remove(t)
    CONF_OPT['animations'] = old_rate


def draw_hint_arrow(cls, movehint, **kwargs):
    """
    Set line parameters for card hint.

    With given hint object, draw line from pile being play to pile building
    with arrow on build end. Override to modify line being drawn.

    .. \_hint_draw(movehint:Hint) -> LineItem Keyword Args
    """
    from .viewlib.canvas import LineItem
    size = cls.images.size
    # Get from pile coord centered on top card of pile to play.
    from_base =  movehint.from_stack.cards[-movehint.ncards]
    from_x, from_y = movehint.from_stack.getPosition(from_base)
    from_offset = movehint.from_stack.getOffset(from_base)
    from_x += (from_offset.x / 2
                    if from_offset.y == 0 and movehint.ncards > 1
                    else size.width / 2)
    from_y += (from_offset.y / 2
                    if from_offset.x == 0 and movehint.ncards > 1
                    else size.height / 2)
    # Get to pile coord centered on top card of pile to build.
    to_x, to_y = movehint.to_stack.getPosition()
    to_x += size.width / 2
    to_y += size.height / 2
    kwargs.update({'arrow': 'last', 'arrowshape': (30, 30, 10)})
    return LineItem(cls.canvas, (from_x, from_y, to_x, to_y,), **kwargs)


def draw_hint_path(self, hint, **kwargs):
    if not hasattr(self, 'grid'):
        return
    from .viewlib.canvas import LineItem
    path = hint.from_stack.cardsBuild(hint.to_stack,
                                      hint.to_stack[:-hint.ncards])
    grid = cls.cols
    card_size = cls.images.size
    card_margin = cls.images.margin
    cols, rows = len(grid), len(grid[0])
    left_top = grid[0][0]
    coord = []
    # Convert relative grid path to absolute path
    for x, y in path:
        # get size from left top for relative grid point
        size = Size(w=left_top.x + card_size.w * (x - 1),
                    h=left_top.y + card_size.h * (y - 1))
        # append xcoord
        if x == 0:                                        # Left-most col
            coord.append(card_margin.x)
        elif x == cols + 1:                               # Right-most col
            coord.append(int(size.w + card_margin.x))
        else:                                             # Inner col
            coord.append(int(size.w + card_size.w / 2))
        # append ycoord
        if y == 0:                                        # Top-most row
            coord.append(card_margin.y)
        elif y == rows + 1:                               # Bottom-most row
            coord.append(int(size.h + card_margin.y))
        else:                                             # Inner row
            coord.append(int(size.h + card_size.h / 2))
    return LineItem(cls.canvas, coord, **kwargs)


class AnimateWin(object):
    """
    Card animation for solving a game.
    """
    FRAME_DELAY = 80
    CYCLE_LEN = 60

    def __init__(self, canvas, animate=True):
        """
        __init__(canvas, animate=True) -> None

        :Parameters:
          canvas : Canvas
            Current canvas for Game.

        :Keywords:
          animate : bool
            Peform win animation on animate():

        :IVariables:
          canvas : Canvas
            Current canvas for Game.

          _do_animate : bool
            Peform win animation on animate():

          __timer :  event.after | event.after_idle
            Set None.

          __images : list(...)
            Selected cards to animate set empty.

          __cache : dict
            saved resampled images

          __frame_num : int
            num. of current frame

          __size : tuple(int, int)
        """
        self.canvas = canvas
        self._do_animate = animate
        self.__timer = None
        self.__images = []
        self.__cache = {}
        self.__frame_num = 0
        self.__width = (0, 0,)

    def __event(self):
        """
        __event() -> None

        Animate cards about canvas. Based on code from PyGTK-demo.

        :IVariables:

          __timer : event.after | event.after_idle
            Set after callback for next animation frame

          __cache : dict
            Upadate resampled images.

          __frame_num : int
            num. of current frame
        """
        starttime = time.time()
        images = self.__images
        cache = self.__cache  # cached images
        canvas = self.canvas
        x0 = int(int(canvas.cget('width')) * canvas.xview()[0])
        y0 = int(int(canvas.cget('height')) * canvas.yview()[0])
        width, height = self._size
        cw = self.canvas.winfo_width()
        ch = self.canvas.winfo_height()
        x0 -= (width - cw) / 2
        y0 -= (height - ch) / 2
        n_images = len(images)
        xmid = width / 2.0
        ymid = height / 2.0
        radius = min(xmid, ymid) / 2.0
        f = float(self.__frame_num % self.CYCLE_LEN) / float(self.CYCLE_LEN)
        r = radius + (radius / 3.0) * math.sin(f * 2.0 * math.pi)
        img_index = 0
        iw, ih = game.images.size
        for index, image in zip(range(n_images), images):
            ang = 2.0 * math.pi * index / n_images - f * 2.0 * math.pi
            xpos = x0 + int(xmid + r * math.cos(ang) - iw / 2.0)
            ypos = y0 + int(ymid + r * math.sin(ang) - ih / 2.0)
            func = math.sin if index & 1 else math.cos
            k = max(0.4, func(f * 2.0 * math.pi) ** 2)
            round_k = int(round(k * 100))
            if index not in cache:
                cache[index] = {}
            if round_k in cache[index]:
                tmp = cache[index][round_k]
            else:
                tmp = image.resample(round_k)
                cache[img_index][round_k] = tmp
            id = canvas.create_image(xpos, ypos, image=tmp, anchor='nw')
            self.win_animation.canvas_images.append(id)
            if k > 0.6:
                canvas.tag_raise(id)
        self.__frame_num += 1
        self.__frame_num %= self.CYCLE_LEN
        canvas.update_idletasks()
        # loop
        t = self.FRAME_DELAY - int((time.time() - starttime) * 1000)
        if t > 0:
            self.__timer = self.canvas.after(t, self.__event)
        else:
            self.__timer = self.canvas.after_idle(self.__event)

    def stop(self):
        """
        stop() ->

        :IVariables:
          __timer :  event.after | event.after_idle
            Set None to stop the animation.

          __images : list(...)

          __cache : dict
        """
        if self.__timer:
            self.canvas.after_cancel(self.__timer)
            self.__timer = None
            self.__images = []
            self.cache = {}
            self.canvas.show('game')

    def start(self, cards):
        """
        start(cards):

        :Parameters:
          cards : list(Card...)
            Current cards on canvas for Game.

        :IVariables:
          __timer :  event.after | event.after_idle
            Set initial callback for first animation frame.

          __images : list(Image...)
            Selected cards to animate.

          __size : (int, int,)
            Set to canvas width, height
        """
        if not self._do_animate:
            return
        from random import choice
        self.canvas.hide('game')
        # select some random cards
        cards = cards[:]
        for i in range(min(10, len(cards))):
            card = choice(cards)
            self.__images.append(card._image[1])
            cards.remove(card)
        # compute visible geometry
        self.__size = self.canvas._cinfo_size
        # run win animation in background
        self.canvas.after(self.canvas, 200, self.winAnimationEvent)